In the remainder of this document, we will concentrate our discussions and efforts in describing shared libraries in terms of Microsoft Windows Dynamic Link Libraries (DLL). Creating shared libraries under UNIX versions of LScript can vary by platform, so we will concentrate upon the most popular LightWave 3D platform. Most of the techniques discussed can be applied equally to the UNIX method of generating a LScript-linkable shared library.
Under Microsoft Windows, there are several steps required to generate a DLL library capable of linking with your LScript scripts. First, you must become familiar with the LScript Interface Mechanism. This mechanism, although sparse, allows you to interact directly with an executing LScript. It is defined in the 'lscript.h' header file, which must be included in each Windows DLL source file that you compile.
Several items are defined in the 'lscript.h' header file for your use in writing your library. First, the data types that can be passed between an LScript and a DLL function are provided as manifest constants. Although some of the more complex LScript data types are not currently available to a DLL function, quite a lot can be done with the data types that are.
#define LSINTEGER 1 #define LSNUMBER 2 #define LSSTRING 3 #define LSVECTOR 4 #define LSFILE 5 #define LSNIL 99The next item defined is a structure used to contain the data that is passed between an LScript and the DLL function:
typedef struct _ls_var { int type; union { double number; const char *string; double vector[3]; } data; } LS_VAR;The 'type' member holds one of the data types defined at the head of the file (LSINTEGER, LSNUMBER, etc.). Based upon the 'type', you can extract data from one of the 'data' union members 'number', 'string' or 'vector'. The 'number' member of the union is used to contain both REAL and INTEGER values. For example, if the 'type' of the data is LSINTEGER, then a simple cast of the 'number' member will extract the integer value:
x = (int)...data.member;The next entry in the header is the function prototype to which all DLL functions wishing to interact with an LScript must adhere. Your DLL functions will not use this typedef directly, rather it is include as a guide while creating your functions. The LScript core engine will invoke your DLL function using this prototype:
typedef LS_VAR * (*DLLfunc)(int *count,LS_VAR *,LSFunc *);Because LScript is itself a shared library, it utilizes the resources of the host process that has loaded it (i.e., LightWave 3D). When LScript allocates memory or opens files, it does so by using LightWave 3D's available resources. For this reason, LScript attempts to be a courteous guest in LightWave's house by cleaning up after itself, thus avoiding the introduction of memory leaks into Layout. To accomplish this, LScript tracks all memory allocations that take place during the execution of a script so that, when it terminates, it can free any resources that were not properly released during the script's execution.
LScript expects a guest in its house to be no less courteous. To that end, LScript extends to its guests access to the internal functions that it uses to manage these critical allocations. You are encouraged to take advantage of these functions when your shared library function needs to allocate resources (either memory or files). This is not, however, a license to be sloppy in your handling of these resources. You should maintain a discipline of explicitly allocating and freeing the resources you use, but you should do so using these exported internal LScript functions instead of utilities provided by your standard libraries. In this way, should you accidentally overlook an explicit resource release, LScript can catch it upon termination.
The next entries in the lscript.h header file is a structure definition that contains pointers to, among other things, each of these internal resource-management functions exported by LScript:
typedef struct _lsfunc { char * (*strdup)(const char *); void * (*malloc)(int); void (*free)(void *); void (*info)(const char *); void (*error)(const char *); } LSFunc;If you refer back to the function prototype defined earlier, you will notice that the last parameter to each shared library function is a pointer to a structure of this type, LSFunc. Using this structure pointer, you can access the function pointers contained within to perform your resource management:
LS_VAR *dllfunc(int count,LS_VAR *parameters,LSFunc *func) { ... // allocate an array of 5 LS_VARs vars = (LS_VAR *)(*func->malloc)(sizeof(LS_VAR) * 5); ... // release our allocations... (*func->free)(vars); ... }
These functions are invoked in the same fashion as other function pointers listed in the structure, but they will produce a visible result to the user:
... (*func->error)("severe error: bad parameter"); ...There are two important facts regarding these functions that you need to be aware of. First, the message that you pass to LScript is not the final message that will be displayed. LScript places information at the beginning of your message (and potentially at the end) before it displays the message to the user. For this reason, you should probably follow the guideline that your message should not begin with a capitalized word, and it should never assume that it is all the information that the user will see. For instance, the output from the previous example might appear to the user as:
Line #45, severe error: bad parameterSecond, any invocation of the error() function will set an internal flag that will cause LScript to halt its execution of the current script when the shared library function terminates. Therefore, error() is intended to be used only when a severe error is encountered within a shared library function that will cause subsequent errors to arise later in the script.
char *getVolumeSerialNumber(void) { char tmpPath[MAX_PATH + 1]; char tmpFile[MAX_PATH + 1]; static char volumeSN[50]; BY_HANDLE_FILE_INFORMATION fileInfo; HANDLE hFile; GetTempPath(MAX_PATH,tmpPath); GetTempFileName(tmpPath,"LScript",0,tmpFile); hFile = CreateFile(tmpFile,GENERIC_READ | GENERIC_WRITE, 0,NULL,OPEN_ALWAYS, FILE_ATTRIBUTE_TEMPORARY,NULL); GetFileInformationByHandle(hFile,&fileInfo); sprintf(volumeSN,"%.4X-%.4X",HIWORD(fileInfo.dwVolumeSerialNumber), LOWORD(fileInfo.dwVolumeSerialNumber)); CloseHandle(hFile); DeleteFile(tmpFile); return(&volumeSN); }Now, in its current state, we cannot use this function directly in an LScript. We must modify it to satisfy the LScript linking conditions. The modifications, however, will be comparatively minor.
First, we must change the header of the function to exactly match the typedef set forth in the 'lscript.h' header. Thus,
char *getVolumeSerialNumber(void)becomes
LS_VAR * getVolumeSerialNumber(int *count,LS_VAR *vars,LSFunc *func)Next, we must have a value to return to the LScript. In the original function, we simply returned a character pointer to a static area. This is not sufficient for interfacing with LScript. We must return one or more LS_VAR structures, each holding a single item of LScript-readable data. In the case of this particular function, we will be returning the same character string wrapped in a LS_VAR structure.
We will declare our own private LS_VAR variable in the function. We must also make sure that the variable itself will exist after the function has returned, so we will declare it as "static":
... static LS_VAR var; ...Likewise, the character string we are returning must also continue to exist after the function returns (otherwise, we will be flirting with disaster when LScript tries to access "released" memory). Fortunately, we already have a "permanent" area, defined in the original function, where the character string can reside without fear of violating memory:
... static char volumeSN[50]; ...Now, because we have altered the parameters that the function accepts, we are no longer able to rely on the compiler to detect an incorrect parameter count at compile time. We must therefore check this parameter count at runtime. It is up to the shared library function to ensure that the count of parameters passed into it by LScript is correct:
if(*count != 0) // we take no arguments { (*func->error)("invalid parameter count to DLL function ... "); return(NULL); }In the case of the getVolumeSerialNumber() function, it takes no arguments (i.e., "void"). If any are passed to it, this should be an error.
The main body of the function remains largely unchanged. Once we have ascertained the volume serial number, we then must package it into a format that LScript can digest. In the original function, we simply returned a character pointer to our static string area:
return(&volumeSN);However, for LScript to properly handle the data, it must be wrapped by our LS_VAR structure, and we must indicate to the LScript engine not only their types, but how many data items we are returning. The new exit code of the function looks like this:
var.type = LSSTRING; var.data.string = volumeSN; *count = 1; // how many data items to expect return(&var); }Here, then, is the complete LScript shared library version of the getVolumeSerialNumber() function:
#include <stdio.h> #include <windows.h> #include "lscript.h" LS_VAR * getVolumeSerialNumber(int *count,LS_VAR *vars) { char tmpPath[MAX_PATH + 1]; char tmpFile[MAX_PATH + 1]; static char volumeSN[50]; static LS_VAR var; BY_HANDLE_FILE_INFORMATION fileInfo; HANDLE hFile; if(*count != 0) // we take no arguments return(NULL); GetTempPath(MAX_PATH,tmpPath); GetTempFileName(tmpPath,"LScript",0,tmpFile); hFile = CreateFile(tmpFile,GENERIC_READ | GENERIC_WRITE, 0,NULL,OPEN_ALWAYS, FILE_ATTRIBUTE_TEMPORARY,NULL); GetFileInformationByHandle(hFile,&fileInfo); sprintf(volumeSN,"%.4X-%.4X",HIWORD(fileInfo.dwVolumeSerialNumber), LOWORD(fileInfo.dwVolumeSerialNumber)); CloseHandle(hFile); DeleteFile(tmpFile); var.type = LSSTRING; var.data.string = volumeSN; *count = 1; return(&var); }
A sample Definition file for the DLL function getVolumeSerialNumber() might look like:
LIBRARY test EXPORTS getVolumeSerialNumberYou can save this definition file under any name you wish, as long as you use that name when you compile the DLL library. In the case of this example, we have named it 'dll.def.' If and as you add functions to your DLL library, you should add their names under the "EXPORTS" section of the Definition file before you re-compile.
CFMACH = -D_X86_=1 CFLAGS = -c -W3 $(CFMACH) -DWIN32 -D_WIN32 /O2 /I . $(OPT) LFLAGS = -dll -def:.\dll.def OBJS = test.obj LLIBS = libc.lib kernel32.lib .cpp.dll: cl $(CFLAGS) $*.cpp .c.dll: cl $(CFLAGS) $*.c all : test.dll test.dll: $(OBJS) link $(LFLAGS) -out:test.dll $(OBJS) $(LLIBS) test.obj : test.cIf you are indeed using Microsoft Visual C++ as your development system, a simple invocation of 'nmake' will generate for you a shiny new DLL that you can now link directly into your LScript. Other development systems will likely require modification of the provide makefile, and possibly other files.
library "test.dll"; main { info("Your volume serial number is ",getVolumeSerialNumber()); }If a path is not specified in the 'library' command, then the LScript engine will scan the following locations in the following order to locate your DLL file:
1. The current directory (which may or may not be the same location where the script resides) 2. The Windows system directory (usually Windows\System or WinNT\System32) 3. The LibraryPath entry in the LScript Configuration for this plug-in type (ls-if, ls-or, etc.)